Skip to content

valkey: finish the handshake's SELECT before reporting the client connected - #41967

Open
robobun wants to merge 2 commits into
mainfrom
robobun/edd6d8bd/valkey-handshake-select
Open

robobun wants to merge 2 commits into
mainfrom
robobun/edd6d8bd/valkey-handshake-select

Conversation

@robobun

@robobun robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With a database in the URL (redis://host/3) the client writes HELLO and SELECT 3 together, but became Connected on the HELLO reply: connect() resolved, onconnect ran and the auto-flusher drained the queue with the SELECT reply still due.
  • A command sent from there was pipelined behind the SELECT. When the server rejected the SELECT (ERR DB index is out of range, cluster mode), the command had already run on database 0 and got its error.
  • Cause: handle_hello_response (src/runtime/valkey_jsc/valkey.rs) set Status::Connected before the SELECT reply, and on_auto_flush gates on that status.

Fix

Background

  • The handshake is HELLO 3 [AUTH user pass], then SELECT n when the URL names a database, in one flush. Replies pair with commands by order.
  • The auto-flusher is a deferred task that writes queued commands to the socket. Deferred tasks drain between two replies read in one chunk, so what onconnect sent went out before the SELECT reply was handled.
Notes

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/valkey/reliability/connection-failures.test.ts

…nected

With a database in the URL the client sends HELLO and SELECT together
but became Connected on the HELLO reply. connect() resolved, onconnect
ran and the auto-flusher drained the queue while the SELECT reply was
still due, so a command sent from there was pipelined behind the SELECT.
When the server rejected the SELECT (index out of range, cluster mode)
that command had already run against database 0, and its promise was
rejected with the SELECT's error.

The client now stays Connecting until the SELECT reply arrives. The
accepted HELLO reply is held in pending_hello so connect() still
resolves with it. A rejected SELECT fails the handshake with
ERR_REDIS_SERVER_ERROR and the server's text, and a SELECT reply that
never comes is bounded by connectionTimeout like the rest of the
handshake.
…y on the held HELLO

authenticate() is the one place that decides whether a SELECT goes out
with the handshake, so it sets is_selecting_db_internal there again and
hello_accepted() tests the flag instead of re-deriving it from the
database index. handle_response() tells the two handshake replies apart
by whether a HELLO reply is held.
@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on 1.4.3 (and main at a3e0ab6) against redis 8.0.2 before writing the fix:

import { RedisClient } from "bun";
const db0 = new RedisClient("redis://127.0.0.1:6379");
const client = new RedisClient("redis://127.0.0.1:6379/99", { autoReconnect: false });
console.log(await client.connect().then(() => "resolved", e => e.code), client.connected); // resolved true
console.log(await client.set("k", "v").then(() => "resolved", e => e.message));          // ERR DB index is out of range
console.log(await db0.get("k"));                                                          // "v"  <- written to db 0

With this branch: connect() rejects, connected stays false, nothing is sent after the rejected SELECT, and db0.get("k") is null. The three tests named in the body fail on stock bun and pass on the debug build.

CI (build 112832): the valkey suites pass on every lane. Three jobs are red, none touch this diff:

  • test/js/node/test/parallel/test-crypto-dh-leak.js on x64-asan, an RSS-slop assertion that is also red on main's recent builds (112820, 112829).
  • Both darwin x64 test-bun jobs ran on agent darwin-x64-14.8.9-1 and failed with Cannot find module '_util/numeric.ts' from test/harness.ts (in test/js/sql/local-sql.test.ts) and Module not found .../fetch-leak-test-fixture-6.js (in test/js/web/fetch/fetch-leak.test.ts). Those files exist in the tree, so that agent's checkout was incomplete.

The other entries in the annotation passed on retry. Ready for review.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: db6e6ab0-dda3-48d4-8423-b39001e295b9

📥 Commits

Reviewing files that changed from the base of the PR and between d745f03 and 31150ad.

📒 Files selected for processing (3)
  • src/runtime/valkey_jsc/js_valkey.rs
  • src/runtime/valkey_jsc/valkey.rs
  • test/js/valkey/reliability/connection-failures.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

Valkey connections remain in Connecting until both HELLO and database SELECT responses complete. The client stores the HELLO response during SELECT, updates failure handling, resets handshake state across lifecycle events, and adds reliability tests for ordering and timeouts.

Valkey handshake completion

Layer / File(s) Summary
Handshake state and lifecycle
src/runtime/valkey_jsc/valkey.rs, src/runtime/valkey_jsc/js_valkey.rs
Adds pending_hello, documents handshake state, initializes it in client constructors, and clears it on open and close.
HELLO and SELECT response flow
src/runtime/valkey_jsc/valkey.rs
Separates HELLO acceptance, SELECT response handling, and handshake completion. Connected, onconnect, and connection_ready now follow completed handshake state.
Handshake failure and ordering tests
test/js/valkey/reliability/connection-failures.test.ts
Tests SELECT ordering, callback behavior, server errors, reconnect behavior, and connection timeout handling.

Suggested reviewers: alii, jarred-sumner, dylan-conway

Merge Risk: ⚪ Minimal · up to 31150

Valkey clients now wait for database selection to complete before reporting connection readiness, preventing commands from running against an unselected database. The updated behavior and failure paths are covered without an identified remaining merge-blocking risk.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: completing the SELECT phase before reporting the Valkey client as connected.
Description check ✅ Passed The description explains the problem, fix, behavioral impact, and verification. It does not use the template headings exactly, but it contains the required information and is sufficiently complete.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this and didn't find any bugs. Deferring to a human because it changes the user-visible connect() contract (resolve → reject on a failed SELECT, ERR_REDIS_INVALID_COMMAND → ERR_REDIS_SERVER_ERROR) and sits in the middle of several adjacent handshake PRs the description names.

What was reviewed:

  • pending_hello lifecycle — initialized at both JSValkeyClient construction sites and reset in both on_close() and connect(), matching every existing is_selecting_db_internal reset.
  • handle_response dispatch — pending_hello.take() correctly routes the second handshake reply to handle_select_response, and handshake_complete is now the only Status::Connected transition; get_timeout_interval keying on != Connected is what makes connectionTimeout bound a hung SELECT.
  • New tests — buffered framing via readCommands, port: 0, control-connection round trips instead of sleep, resources released in finally, combined-object .toEqual assertions.
Extended reasoning...

Overview

The PR reorders the Valkey client's handshake state machine so that when the URL specifies a database, the client remains Connecting until the SELECT reply arrives (previously it flipped to Connected on the HELLO reply and handled SELECT afterward). A new pending_hello: Option<RESPValue> field stashes the accepted HELLO while waiting for SELECT, handle_response dispatches handshake replies based on it, and a single handshake_complete() is the one place that sets Connected, resets reconnect state, and calls on_valkey_connect. connection_ready() drops its now-redundant is_selecting_db_internal check, and a rejected SELECT surfaces as RedisError::ServerError instead of InvalidCommand. Tests are updated in test/js/valkey/reliability/connection-failures.test.ts: the existing rejected-SELECT test now asserts connect() rejects and onconnect doesn't fire on the failed handshake, plus two new tests with a stub server that withholds the SELECT reply.

Security risks

None identified. This is internal connection-state sequencing for a database client; no auth, crypto, path, or untrusted-input parsing is touched. The RESP parsing path is unchanged — only which handler a reply is routed to during the handshake.

Level of scrutiny

Medium-high. The mechanical change is small and the state resets are complete (both construction sites, on_close, connect()), but it alters user-observable semantics: client.connected stays false longer, onconnect fires later, connect() now rejects where it previously resolved on a failed SELECT, the error code changes, and with enableOfflineQueue: false a command in the SELECT window is now rejected instead of accepted. The PR description also names four adjacent in-flight PRs (#39542, #41829, #41645, #36071) touching the same functions, so a maintainer should confirm the composition and the intended error-code contract.

Other factors

The lifecycle audit checks out: pending_hello is reset alongside is_selecting_db_internal at every site that resets the latter, and get_timeout_interval already keys on status != Connected, so extending the Connecting window is what makes connectionTimeout bound a never-answered SELECT — no new timer wiring needed. Tests follow repo conventions closely (buffered framing, port: 0, control-connection round trips to turn the event loop rather than sleep, Promise.withResolvers for the SELECT-arrived signal, finally cleanup, combined .toEqual objects). No CODEOWNERS entry covers src/runtime/valkey_jsc/. The exit reason was dry_streak, so the hunt ran to completion.

@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:27 AM PT - Sep 8th, 2026

❌ @robobun, your commit 31150ad has 1 failures in Build #112832 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41967

That installs a local version of the PR into your bun-41967 executable, so you can run:

bun-41967 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants